# 本地路径处理 path
30 秒速记
path.join只拼接 + 规范化,结果可能是相对路径;path.resolve从右往左拼,直到凑出绝对路径,不够就补process.cwd()join('/a', '/b')=/a/b,resolve('/a', '/b')=/b,这是两者最直观的区别- 相对路径默认相对
process.cwd(),不是相对文件所在目录;要稳定就基于__dirname __dirname只在CommonJS有;ESM用import.meta.dirname(Node 20.11+)或fileURLToPath(import.meta.url)- 用户传的路径先
resolve再检查没逃出根目录,防../../etc/passwd
path 模块就是用来正确拼接和解析文件路径的,最常用的是 join 和 resolve,两者看着像,语义不一样。 join 只是把片段用当前系统的分隔符连起来并清理掉 ..、多余斜杠,结果可能还是相对路径;resolve 像在终端里依次 cd,从右往左拼,遇到绝对路径就停,最后还不是绝对路径就补上当前工作目录。写脚本时最常见的坑是相对路径相对的是 process.cwd(),换个目录执行就找不到文件,所以我一般用 path.join(__dirname, 'xxx') 定位。
const path = require('path')
path.join('/a', 'b', '../c') // '/a/c'
path.join('/a', '/b') // '/a/b'
path.resolve('/a', '/b') // '/b' 遇到绝对路径就重新开始
path.resolve('src') // '/当前工作目录/src'
path.join('src') // 'src' 仍是相对路径
理解「本地路径处理 path」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 filesystem 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
path 模块处理的是路径字符串,不碰文件系统,所以它不会告诉你文件存不存在,只负责把路径算对。日常最常用的几个 API:
const path = require('path')
const file = '/home/user/project/src/index.test.js'
path.dirname(file) // '/home/user/project/src'
path.basename(file) // 'index.test.js'
path.basename(file, '.js') // 'index.test'
path.extname(file) // '.js' 只取最后一个点之后的部分
path.parse(file) // { root: '/', dir: '/home/user/project/src', base: 'index.test.js', ext: '.js', name: 'index.test' }
path.relative('/a/b/c', '/a/d') // '../../d'
path.isAbsolute('./x') // false
path.sep // POSIX 下是 '/',Windows 下是 '\\'
join 和 resolve 的区别可以这样记:join 是「拼字符串再整理」,resolve 是「模拟一路 cd 之后所在的位置」。所以 path.resolve('a', 'b') 会得到 process.cwd() 开头的绝对路径,而 path.join('a', 'b') 只是 a/b。
脚本里定位文件时,最容易出错的是「相对路径到底相对谁」。fs.readFileSync('./config.json') 里的 ./ 相对的是启动进程时所在的目录,不是这个 JS 文件所在的目录。同一个脚本,在项目根目录执行能找到,换到上一级目录执行就找不到。解决办法是基于文件自身位置拼路径:
// CommonJS
const configPath = path.join(__dirname, 'config.json')
// ESM,Node 20.11+
const configPath2 = path.join(import.meta.dirname, 'config.json')
// ESM,更老的版本
import { fileURLToPath } from 'node:url'
const dir = path.dirname(fileURLToPath(import.meta.url))
处理用户输入的路径(比如静态文件服务、上传下载)时,一定要先算出绝对路径,再确认它还在允许的根目录里:
function safeJoin(root, userPath) {
const target = path.resolve(root, userPath)
const rel = path.relative(root, target)
if (rel.startsWith('..') || path.isAbsolute(rel)) throw new Error('非法路径')
return target
}
safeJoin('/srv/static', 'img/a.png') // '/srv/static/img/a.png'
safeJoin('/srv/static', '../../etc/passwd') // 抛错
跨平台方面,path 默认按当前系统的规则工作。如果要在 Linux 上测试 Windows 路径,或者需要始终输出 / 分隔的 URL 风格路径,可以显式使用 path.win32 和 path.posix。
面试官追问
追问 1在 macOS 上能找到模板文件,Windows 的 CI 上找不到,常见原因是什么?
多半是手拼了 'src/' + name 或写死了 /,再就是文件名大小写,macOS 默认不区分大小写,Linux 区分。拼路径一律用 path.join,比较路径前先 path.normalize。
追问 2用户传了 ../../etc/passwd 当文件名,怎么防?
先 const target = path.resolve(root, input),再检查 target.startsWith(root + path.sep),不满足就拒绝。只过滤 .. 字符串不够,还有 URL 编码、绝对路径这些绕法;有软链接的话还要用 fs.realpathSync 解析后再比。
追问 3ESM 项目里写 __dirname 报 ReferenceError,怎么办?
ESM 里没有这个变量。Node 20.11 起可以直接用 import.meta.dirname;老版本写 path.dirname(fileURLToPath(import.meta.url)),fileURLToPath 从 node:url 引入。
追问 4path.resolve 和 path.join 什么时候该用哪个?
需要一个确定的绝对路径时用 resolve,比如配置 webpack 的 output.path;只是把几段拼起来,基准已经是 __dirname 这类绝对路径时,用 join 更直观。踩坑最多的是 resolve 的参数里混进一个以 / 开头的片段,前面的全被丢了。
追问 5单测里想验证 Windows 路径的处理结果,但 CI 跑在 Linux 上,怎么写?
直接用 path.win32.join('C:\\a', 'b'),结果是 'C:\\a\\b',在任何系统上都按 Windows 规则算。同理 path.posix 固定按 / 处理,生成 URL 路径时也常用它。
# 模块概览
30 秒速记
path只做字符串层面的路径计算,不碰磁盘,不知道文件存不存在- 按用途分五组记:拆(
dirname/basename/extname)、拼(join/resolve)、规范化(normalize)、分解组合(parse/format)、相对路径(relative) - 平台差异是难点:
Windows分隔符是\、有盘符,POSIX是/;默认path跟着当前系统走,要固定行为就用path.posix/path.win32 __dirname只在CommonJS里有;ESM用fileURLToPath(import.meta.url),Node 20.11+可以直接用import.meta.dirname
path 是 Node.js 里专门算路径字符串的模块,它不读磁盘,只负责把路径拆开、拼起来、算干净。 大家觉得它乱,一是接口多、名字像,二是同一个方法在 Windows 和 Linux
